# Copyright (c) 2018 Presto Labs Pte. Ltd.
# Author: inkyu

import datetime
import logging
from collections import namedtuple
from datetime import timedelta
from enum import Enum

from coin.base.datetime_util import to_datetime
from coin.strategy.tool.log_v1.util import find_log_pos_by_timestamp
from coin.strategy.tool.log_v2.util import datetime_from_poslog, datetime_from_glog, extract_datetime_slow
from coin.exchange.okex_futures.kr_rest.futures_product import OkexFuturesProduct
from coin.exchange.huobi_futures.kr_rest.futures_product import HuobiFuturesProduct

FillStat = namedtuple('FillStat', ['timestamp', 'side'])


class _OrderSide(Enum):
  UNKNOWN = 0
  BUY = 1
  SELL = 2


def side_from_sign(signed):
  if int(signed) == 1:
    return _OrderSide.BUY
  elif int(signed) == -1:
    return _OrderSide.SELL
  else:
    return _OrderSide.UNKNOWN


def datetime_diff_or_none(dt2, dt1):
  if dt2 is None or dt1 is None:
    return None
  return dt2 - dt1


def format_timedelta_long(time_delta):
  if time_delta is None:
    return ''

  ts = time_delta.total_seconds()
  h = int(ts / 3600)
  m = int(ts / 60) % 60
  s = int(ts % 60)
  return '%02d:%02d:%02d' % (h, m, s)


def format_timedelta_short(time_delta):
  if time_delta is None:
    return ''
  parsed = int(time_delta.total_seconds())
  return '%.2fH' % (parsed / (60.0 * 60.0))


def format_fill_stat(fill_stat, cur_time):
  if fill_stat is None:
    return ''

  timediff = cur_time - fill_stat.timestamp
  if fill_stat.side == _OrderSide.BUY:
    side_str = '+'
  elif fill_stat.side == _OrderSide.SELL:
    side_str = '-'
  else:
    side_str = ''
  return '%s%s' % (format_timedelta_long(timediff), side_str)


def format_time_marked(time_marked):
  if time_marked is None:
    return ''
  return datetime.datetime.strftime(time_marked, "%m-%d %H:%M")


def side_from_str(side_str):
  if side_str in ('BUY',
                  'OkexFuturesOrderSide.BUY_OPEN',
                  'OkexFuturesOrderSide.BUY_CLOSE',
                  'HuobiFuturesOrderSide.BUY_OPEN',
                  'HuobiFuturesOrderSide.BUY_CLOSE'):
    return _OrderSide.BUY
  elif side_str in ('SELL',
                    'OkexFuturesOrderSide.SELL_OPEN',
                    'OkexFuturesOrderSide.SELL_CLOSE',
                    'HuobiFuturesOrderSide.SELL_OPEN',
                    'HuobiFuturesOrderSide.SELL_CLOSE'):
    return _OrderSide.SELL
  else:
    return _OrderSide.UNKNOWN


def _extract_datetime_1(line):
  timestamp_format = '%Y-%m-%d %H:%M:%S,%f'  # Fucking european style
  splitted = line.split()
  try:
    dt = datetime.datetime.strptime('%s %s' % (splitted[1], splitted[2]), timestamp_format)
    return dt
  except (ValueError, IndexError):
    return None


def _extract_datetime_2(line):
  timestamp_format = 'I%m%d %H:%M:%S.%f'
  splitted = line.split()
  try:
    dt = (datetime.datetime.strptime('%s %s' % (splitted[0], splitted[1]),
                                     timestamp_format).replace(year=datetime.date.today().year))
    return dt
  except (ValueError, IndexError):
    return None


def _datetime_from_bitflyer_log(line):
  dt = None
  lstripped = line.lstrip(' ')
  if lstripped[0:4] == 'INFO':
    dt = extract_datetime_slow(line)
  return dt


def extract_datetime(line):
  dt = _extract_datetime_1(line)
  if dt is not None:
    return dt
  return _extract_datetime_2(line)


class ParserState(Enum):
  READY = 0
  HAS_DATETIME = 1
  HAS_BALANCE = 2
  DONE = 3


class LogParser(object):
  def __init__(self,
               owner,
               name,
               group,
               quote,
               time_current,
               balance_marker='BTC',
               position_marker='USD',
               fill_collector=None,
               flexibility=timedelta(minutes=10),
               adjust_withdrawal=True,
               use_position_1=False,
               dump_duration=None,
               spot_mode=False):
    self._time_current = time_current
    self._time_mark = (time_current - flexibility).replace(hour=0,
                                                           minute=0,
                                                           second=0,
                                                           microsecond=0)
    self._time_4h = time_current - timedelta(hours=4)
    self._time_24h = time_current - timedelta(hours=24)
    self._time_begin = time_current - timedelta(hours=28)
    self._last_fills = {}

    self._adjust_withdrawal = adjust_withdrawal
    self._adjust_nonunit = {
        'BTC': 6., 'JPY': 2000000, 'EOS': 2000, 'ETH': 200, 'USDT': 20000, 'LTC': 200
    }
    self._adjust_unit = {
        'BTC': 1., 'JPY': 1000000., 'EOS': 1000, 'ETH': 10, 'USDT': 5000, 'LTC': 50
    }
    self._adjust_tolerance = {
        'BTC': 0.05, 'JPY': 50000., 'EOS': 3, 'ETH': 0.1, 'USDT': 20, 'LTC': 1
    }
    self._value_offset = 0.

    self._owner = owner
    self._name = name
    self._group = group
    self._quote = quote
    self._time = None
    self._time_marked = None
    self._last_log_time = None
    self._state = ParserState.READY
    self._fill_collector = fill_collector
    self._use_position_1 = use_position_1
    self._balance_marker = balance_marker
    self._position_marker = position_marker

    self._position_last = None
    self._position_last_1 = None
    self._position_last_2 = None

    self._value_last = None
    self._value_last_1 = None
    self._value_last_2 = None
    self._value_mark = None
    self._value_4h = None
    self._value_24h = None
    self._turnover_mark = 0
    self._turnover_4h = 0
    self._turnover_24h = 0

    self._balance_dump = []
    self._position_dump = []
    self._turnover_dump = []

    self._processing_time = None
    self._spot_mode = spot_mode
    self._total = None
    self._balance = None
    self._aggregate_pos = self._spot_mode and position_marker in ("USD", "KRW", "EUR", "USDT")

    if dump_duration:
      self._time_begin = time_current - dump_duration
      self._time_mark = time_current - dump_duration

  def handle_price(self, line: str):
    if line.startswith('*'):
      splitted = line.split()
      if not self._spot_mode:
        if len(splitted) == 5 and splitted[1] == self._balance_marker:
          position_1 = None
          if self._use_position_1:
            position_1 = float(splitted[3])
          return [float(splitted[2]), position_1, float(splitted[4])]
        elif len(splitted) == 3 and splitted[1] == self._balance_marker:
          return [float(splitted[2]), None, float(splitted[2])]
        elif len(splitted) == 4 and splitted[1] == self._balance_marker:
          return [float(splitted[2]), None, float(splitted[2])]
      else:
        if len(splitted) == 4 and splitted[1] == self._balance_marker:
          return [float(splitted[3]), None, float(splitted[3])]

  def handle_position(self, line: str):
    if line.startswith('*'):
      splitted = line.split()

      if len(splitted) > 1:
        splitted[1] = splitted[1].replace('OkexFutures', 'Okex')
        splitted[1] = splitted[1].replace('HuobiFutures', 'Huobi')
      if isinstance(self._position_marker, HuobiFuturesProduct):
        pr = self._position_marker
        prod = HuobiFuturesProduct.FromCurrencyPair2AndContractType(pr.base,
                                                                    pr.quote,
                                                                    pr._contract_type,
                                                                    current_datetime=self._time)
        position_marker = [prod.symbol, prod.full_symbol]
      elif isinstance(self._position_marker, OkexFuturesProduct):
        pr = self._position_marker
        prod = OkexFuturesProduct.FromCurrencyPair2AndContractType(pr.base,
                                                                   pr.quote,
                                                                   pr._contract_type,
                                                                   current_datetime=self._time)
        position_marker = [prod.symbol, prod.full_symbol]
      else:
        position_marker = [self._position_marker]

      if len(splitted) == 5 and splitted[1] in position_marker:
        return [float(splitted[2]), float(splitted[3]), float(splitted[4])]
      elif len(splitted) == 4 and splitted[1] in position_marker:
        return [float(splitted[2]), 0., float(splitted[2])]
      elif (self._spot_mode and self._aggregate_pos
            and (splitted[1] in position_marker or splitted[1] in [self._balance_marker])):
        if splitted[1] == self._balance_marker:
          self._total = float(splitted[-1])
        else:
          self._balance = float(splitted[-1])
        if self._total is not None and self._balance is not None:
          pnl_pos = self._total - self._balance
        else:
          return None
        return [pnl_pos, 0., pnl_pos]
      elif len(splitted) == 7 and not self._aggregate_pos and splitted[1] in position_marker:
        pnl_pos = float(splitted[3]) - float(splitted[2])
        return [pnl_pos, 0., pnl_pos]
      elif len(splitted) == 2 and splitted[1] in position_marker:
        return [0., 0., 0.]

  def detect_position_datetime(self, line: str):
    return datetime_from_poslog(line)

  def detect_log_datetime(self, line: str):
    dt = datetime_from_glog(line, self._time_current.year)
    if dt is None:
      dt = _datetime_from_bitflyer_log(line)
    return dt

  def update_values(self, value, value_1, value_2):
    if 0 in (value, value_1, value_2):
      return

    if (self._adjust_withdrawal and self._quote in ('BTC', 'JPY', 'ETH', 'EOS', 'LTC', 'USDT')
        and self._value_last is not None):
      adj_nonunit = self._adjust_nonunit[self._quote]
      adj_unit = self._adjust_unit[self._quote]
      adj_tor = self._adjust_tolerance[self._quote]
      adj_value = value + self._value_offset
      diff = adj_value - self._value_last
      if abs(diff) > adj_unit - adj_tor:
        for i in range(1, 20):
          adj_amt = i * adj_unit
          if (adj_amt - adj_tor) < diff < (adj_amt + adj_tor):
            self._value_offset -= adj_amt
            break
          elif -(adj_amt + adj_tor) < diff < -(adj_amt - adj_tor):
            self._value_offset += adj_amt
            break
        if self._value_offset == 0 and abs(diff) > adj_nonunit:
          self._value_offset -= diff
      value = value + self._value_offset

    self._balance_dump.append({'time': self._time, 'value': value})
    if self._value_mark is None or self._time < self._time_mark:
      self._value_mark = value
      self._time_marked = self._time
    if self._value_4h is None or self._time < self._time_4h:
      self._value_4h = value
    if self._value_24h is None or self._time < self._time_24h:
      self._value_24h = value
    self._value_last = value
    self._value_last_1 = value_1
    self._value_last_2 = value_2

  def update_positions(self, position, position_1, position_2):
    self._position_last = position
    self._position_last_1 = position_1
    self._position_last_2 = position_2

    net_pos = (position_1 or 0.) + (position_2 or 0.)
    self._position_dump.append({'time': self._time, 'value': net_pos})

  def add_turnover(self, time, turnover):
    if time > self._time_mark:
      self._turnover_mark += turnover
    if time > self._time_4h:
      self._turnover_4h += turnover
    if time > self._time_24h:
      self._turnover_24h += turnover

    if not self._turnover_dump:
      self._turnover_dump.append({'time': time, 'turnover': turnover})
    else:
      cum_turnover = self._turnover_dump[-1]['turnover'] + turnover
      self._turnover_dump.append({'time': time, 'turnover': cum_turnover})

  def handle_fill_dump(self, line: str):
    comma_separated = line.split(',')
    if len(comma_separated) == 13 and comma_separated[0] == '"FILL_DUMP"':
      timestamp = to_datetime(int(comma_separated[1]))
      if timestamp > self._time_current:
        self._state == ParserState.DONE
        return False
      exchange_id = int(comma_separated[3])
      side = side_from_sign(comma_separated[4])
      _fill_price = float(comma_separated[5])
      fill_qty = float(comma_separated[6])
      self._last_fills[exchange_id] = FillStat(timestamp, side)
      self.add_turnover(timestamp, abs(fill_qty))
      return True

    elif len(comma_separated) == 9 and comma_separated[0] == '"FILL_DUMP"':
      timestamp = to_datetime(int(comma_separated[1]))
      if timestamp > self._time_current:
        self._state == ParserState.DONE
        return False
      exchange_id = 2
      side = side_from_sign(comma_separated[3])
      _fill_price = float(comma_separated[4])
      fill_qty = float(comma_separated[5]) * 10
      self._last_fills[exchange_id] = FillStat(timestamp, side)
      self.add_turnover(timestamp, abs(fill_qty))
      return True

    elif len(comma_separated) == 12 and comma_separated[0] == '"FILL_DUMP"':
      # Bitfinex: we must consider this
      timestamp = to_datetime(int(comma_separated[1]))
      if timestamp > self._time_current:
        self._state == ParserState.DONE
        return False
      exchange_id = 2
      exchange = str(comma_separated[3])
      symbol = str(comma_separated[4])
      side = side_from_sign(comma_separated[5])
      fill_price = float(comma_separated[6])
      fill_qty = float(comma_separated[7])
      if exchange in ('"OkexFutures"', '"HuobiFutures"', '"Bitmex"'):
        if isinstance(self._position_marker, (HuobiFuturesProduct, OkexFuturesProduct)):
          target_symbol = [self._position_marker.symbol, self._position_marker.full_symbol]
        else:
          target_symbol = [self._position_marker]
        if symbol.strip('"') not in target_symbol:
          return False
        else:
          self._last_fills[exchange_id] = FillStat(timestamp, side)
      else:
        self._last_fills[exchange_id] = FillStat(timestamp, side)

      if exchange in ('"OkexFutures"', '"HuobiFutures"'):
        if 'BTC-USD' in symbol:
          self.add_turnover(timestamp, abs(fill_qty * 100))
        else:
          self.add_turnover(timestamp, abs(fill_qty * 10))
      elif exchange == '"Bitmex"':
        self.add_turnover(timestamp, abs(fill_qty))
      elif exchange == '"Kraken"' and symbol.find("PERPETUAL") >= 0:
        self.add_turnover(timestamp, abs(fill_qty))
      else:
        self.add_turnover(timestamp, abs(fill_qty * fill_price))
      return True

    return False

  def handle_fill_collector(self, line: str):
    timestamp_format = '%Y-%m-%d %H:%M:%S,%f'  # Fucking european style
    side = None
    result = None
    splitted = line.split()
    fill_qty = None
    timestamp = None
    exchange_id = None

    # DEBUG 2018-03-16 17:35:21,280 Bitmex] FILL XBTUSD SELL 35000.000000@8552.0
    if (len(splitted) == 8 and splitted[0] in ('DEBUG', 'INFO') and splitted[3] == 'Bitmex]'
        and splitted[4] == 'FILL'):
      exchange_id = 2
      timestamp = datetime.datetime.strptime("%s %s" % (splitted[1], splitted[2]), timestamp_format)
      side = side_from_str(splitted[6])
      fill_qty_s, fill_price_s = splitted[7].split('@')
      fill_qty = float(fill_qty_s)
      _fill_price = float(fill_price_s)
      result = True

    # INFO 2018-02-26 10:43:40,355 SoftArbStrategy] [1] FILL OkexFuturesOrderSide.BUY_OPEN 1.000000 @ 9549.8500
    elif (len(splitted) == 10 and splitted[0] == 'INFO' and splitted[3] == 'SoftArbStrategy]'
          and splitted[8] == '@'):
      exchange_id = 1
      timestamp = datetime.datetime.strptime("%s %s" % (splitted[1], splitted[2]), timestamp_format)
      side = side_from_str(splitted[6])
      fill_qty = float(splitted[7]) * 100
      _fill_price = float(splitted[9])
      result = True

    # INFO 2018-06-13 18:30:10,472 Bitflyer order_gateway.py:275] FILL FX_BTC_JPY SELL 1.000000 @ 721370.000000
    # I0725 08:11:47.967512 139756067235648 order_gateway.py:287] FILL FX_BTC_JPY  BUY 1.400000 @ 965684.000000
    elif (len(splitted) >= 6 and 'FILL' in splitted and 'FX_BTC_JPY' in splitted):
      base_idx = splitted.index('FILL')
      exchange_id = 2
      timestamp = extract_datetime(line)
      side = side_from_str(splitted[base_idx + 2])
      _fill_price = float(splitted[base_idx + 5])
      fill_qty = float(splitted[base_idx + 3]) * _fill_price
      result = True

    if result:
      if timestamp > self._time_current:
        self._state == ParserState.DONE
        return None
      self._last_fills[exchange_id] = FillStat(timestamp, side)
      self.add_turnover(timestamp, fill_qty)
    return result

  def parse_log_line(self, line: str):
    if self._fill_collector == 'fill_collector':  # Parse the log directly
      if self.handle_fill_collector(line):
        return
    elif self._fill_collector == 'csv':
      if self.handle_fill_dump(line):
        return

    datetime_parsed = self.detect_position_datetime(line)
    if datetime_parsed is not None:
      self._time = datetime_parsed
    if self._state == ParserState.READY:
      datetime_parsed = self.detect_position_datetime(line)
      if datetime_parsed is not None:
        if datetime_parsed > self._time_current:
          self._state == ParserState.DONE
          return
        self._state = ParserState.HAS_DATETIME
      self._time = datetime_parsed
    elif self._state == ParserState.HAS_DATETIME:
      values = self.handle_price(line)
      if values is not None:
        value, value_1, value_2 = values
        self._state = ParserState.HAS_BALANCE
        self.update_values(value, value_1, value_2)
    elif self._state == ParserState.HAS_BALANCE:
      positions = self.handle_position(line)
      if positions is not None:
        position, position_1, position_2 = positions
        self._state = ParserState.READY
        self.update_positions(position, position_1, position_2)

  def get_result(self):
    mark_time = ''
    if self._time_marked:
      mark_time = '%s (%s)' % (format_time_marked(self._time_marked),
                               format_timedelta_short(
                                   datetime_diff_or_none(self._time_current, self._time_marked)))
    return {
        'owner':
            self._owner,
        'name':
            self._name,
        'group':
            self._group,
        'quote':
            self._quote,
        'account_value':
            self._value_last,
        'mark_value':
            self._value_mark,
        'pnl_mark':
            datetime_diff_or_none(self._value_last, self._value_mark),
        'pnl_4h':
            datetime_diff_or_none(self._value_last, self._value_4h),
        'pnl_24h':
            datetime_diff_or_none(self._value_last, self._value_24h),
        'turnover_mark':
            self._turnover_mark,
        'turnover_4h':
            self._turnover_4h or None,
        'turnover_24h':
            self._turnover_24h or None,
        'balance_1':
            self._value_last_1 or None,
        'balance_2':
            self._value_last_2 or None,
        'position_1':
            self._position_last_1 or None,
        'position_2':
            self._position_last_2 or None,
        'fill_1':
            format_fill_stat(self._last_fills.get(1), self._time_current),
        'fill_2':
            format_fill_stat(self._last_fills.get(2), self._time_current),
        'mark_time':
            mark_time,
        'balance_dump':
            self._balance_dump,
        'position_dump':
            self._position_dump,
        'processing_time':
            self._processing_time,
        'last_log':
            format_timedelta_long(datetime_diff_or_none(self._time_current, self._last_log_time))
    }

  def run(self, log_path):
    begin_dt = datetime.datetime.now()

    try:
      pos = find_log_pos_by_timestamp(log_path, self._time_begin)
    except FileNotFoundError:
      return self.get_result()
    except Exception:
      logging.exception('Error parsing on %s', log_path)
      raise

    with open(log_path, 'r', errors='ignore') as log_file:
      log_file.seek(pos)
      for line in log_file.readlines():
        line_time = self.detect_log_datetime(line)
        if line_time is not None:
          self._last_log_time = min(line_time, self._time_current)
        self.parse_log_line(line)
        if self._state == ParserState.DONE:
          break

    end_dt = datetime.datetime.now()
    self._processing_time = end_dt - begin_dt
    return self.get_result()
